package com.eva.shiro;

import com.eva.framework.config.AppConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Shiro缓存
 */
@Scope(value = "prototype")
@Slf4j
@Component
public class ShiroCache implements Cache<Object, Serializable> {

    private static final String KEY_PREFIX = "shiro:session:";

    @Resource
    private AppConfig appConfig;

    @Resource(name = "sessionRedisTemplate")
    private RedisTemplate<Object, Serializable> redisTemplate;

    @Override
    public Serializable get(Object key) throws CacheException {
        if (key == null) {
            return null;
        }
        return redisTemplate.opsForValue().get(getKey(key));
    }

    /**
     * put方法在创建新的会话和更新会话时均会被调用
     *
     * @param key sessionId
     * @param value 会话对象
     * @return 会话对象
     * @throws CacheException 缓存异常
     */
    @Override
    public Serializable put(Object key, Serializable value) throws CacheException {
        if (key == null) {
            return null;
        }
        // 会话已存在 && 固定会话时长模式
        if (Boolean.TRUE.equals(redisTemplate.hasKey(getKey(key))) && "FIXED".equals(appConfig.getSession().getMode())) {
            Long expire = redisTemplate.opsForValue().getOperations().getExpire(getKey(key));
            // 会话存在超时时间，重新写入会话对象，并标记过期时长为剩余时长
            if (expire != null) {
                redisTemplate.opsForValue().set(getKey(key), value, expire, TimeUnit.SECONDS);
            }
            // 没有过期时长，直接重新写入会话时长
            else {
                redisTemplate.opsForValue().set(getKey(key), value);
            }
            return value;
        }
        // 会话不存在或交互式会话，直接写入缓存
        redisTemplate.opsForValue().set(getKey(key), value, appConfig.getSession().getExpire(), TimeUnit.SECONDS);
        return value;
    }

    @Override
    public void clear() throws CacheException {
        Set<Object> keys = this.keys();
        redisTemplate.delete(keys);
    }

    @Override
    public int size() {
        return this.keys().size();
    }

    @Override
    public Set<Object> keys() {
        Set<Object> keys = redisTemplate.keys(KEY_PREFIX + "*");
        if (org.apache.shiro.util.CollectionUtils.isEmpty(keys)) {
            return Collections.emptySet();
        }
        return keys;
    }

    @Override
    public Collection<Serializable> values() {
        Collection<Serializable> values = new ArrayList<>();
        Set<Object> keys = this.keys();
        if (CollectionUtils.isEmpty(keys)) {
            return values;
        }
        for (Object k : keys) {
            values.add(redisTemplate.opsForValue().get(k));
        }
        return values;
    }

    @Override
    public Serializable remove(Object key) throws CacheException {
        if (key == null) {
            return null;
        }
        Serializable value = this.get(getKey(key));
        redisTemplate.delete(getKey(key));
        return value;
    }

    /**
     * 重新延长缓存键的存储时长
     *
     * @param key 缓存键
     */
    public void relive(Object key) {
        if (key == null) {
            return;
        }
        redisTemplate.opsForValue().getOperations().expire(getKey(key), appConfig.getSession().getExpire(), TimeUnit.SECONDS);
    }

    /**
     * 获取SessionId的存储键
     *
     * @param key sessionId
     * @return SessionId的存储键
     */
    private Object getKey (Object key) {
        if (key instanceof PrincipalCollection) {
            return KEY_PREFIX + getRedisKeyFromPrincipalIdField((PrincipalCollection)key);
        }
        return (key instanceof String ? (KEY_PREFIX + key) : key);
    }

    /**
     * 获取redis cache key
     */
    private String getRedisKeyFromPrincipalIdField(PrincipalCollection key) {
        Object principalObject = key.getPrimaryPrincipal();
        if (principalObject instanceof String) {
            return principalObject.toString();
        } else {
            Method principalIdGetter = this.getPrincipalIdGetter(principalObject);
            return this.getIdObj(principalObject, principalIdGetter);
        }
    }

    private Method getPrincipalIdGetter(Object principalObject) {
        Method principalIdGetter;
        try {
            principalIdGetter = principalObject.getClass().getMethod("getId");
            return principalIdGetter;
        } catch (NoSuchMethodException e) {
            throw new SerializationException(e.getMessage(), e);
        }
    }

    private String getIdObj(Object principalObject, Method principalIdGetter) {
        try {
            Object idObj = principalIdGetter.invoke(principalObject);
            return idObj.toString();
        } catch (Exception e) {
            throw new SerializationException(e.getMessage(), e);
        }
    }
}
